package org.carabiner.infinitest;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;

import javax.swing.JOptionPane;

/**
 * 
 * @author <a href="mailto:benrady@gmail.com"Ben Rady</a>
 */
class ExternalProcessTestRunner implements TestRunner
{
  private static final int DEFAULT_PORT = 4120;
  private TestRunnerEventSupport eventSupport;
  private Process process;
  private int currentPort;
  private int maxMemoryInMegs = 64;
  private int MAX_MEMORY_ALLOWED = 256;
  private Class<? extends TestRunner> runnerClass;
  private boolean killTestProcess;  

  public ExternalProcessTestRunner(Class<? extends TestRunner> testRunnerClass) throws IOException
  {
    eventSupport = new TestRunnerEventSupport(this);
    classpath = InfinitestUtils.getCleanClasspath(System.getProperty("java.class.path"));
    newProcessClassname = TestRunnerProcess.class.getName();
    runnerClass = testRunnerClass;
    startProcess();
    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable()
    {
      public void run()
      {
        synchronized (ExternalProcessTestRunner.this)
        {
          process.destroy();
        }
      }
    }));
  }

  synchronized int dispose() throws InterruptedException
  {
    process.destroy();
    process.waitFor();
    return process.exitValue();
  }

  private synchronized void startProcess() throws IOException
  {
    int triesLeft = 5;
    do
    {      
      // System.out.print("Spawning new process...");
      if (triesLeft <= 1)
      {     
        currentPort = DEFAULT_PORT;
      }
      process = launchProcess(currentPort);
      BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
      currentPort = getPortNumber(reader);
      if (currentPort == 0)
      {
        process.destroy();
        // System.out.println("failed!");
      }
      reader.close();
      triesLeft--;
    } while (currentPort == 0 && triesLeft > 0);    
    if (currentPort == 0)
    {     
      int returnCode = process.exitValue();
      if (returnCode != 0)
      {
        writeErrorToOut(process);
      }
      throw new IOException("External process terminated prematurely with code " + returnCode);
    }
  }

  private void writeErrorToOut(Process failedProcess) throws IOException
  {
    BufferedReader errorStream = new BufferedReader(new InputStreamReader(failedProcess.getErrorStream()));    
    for(String line= errorStream.readLine(); line != null; line = errorStream.readLine())
    {
      System.out.println(line);
    }
  }

  private static final String JAVA_EXECUTABLE = "java";
  private String classpath;
  private String newProcessClassname;
  Process launchProcess(int portNum) throws IOException
  {
    ProcessBuilder builder = new ProcessBuilder();    
    String memorySetting = "-mx" + maxMemoryInMegs + "m";    
    builder.command(new String[]
    {
        JAVA_EXECUTABLE,
        "-cp",
        classpath,
        memorySetting,
        newProcessClassname,
        runnerClass.getName(),
        String.valueOf(portNum)        
    });    
            
    return builder.start();
  }

  int getPortNumber(BufferedReader reader) throws IOException
  {
    try
    {
      int triesLeft = 10;
      while (!reader.ready() && triesLeft > 0)
      {
        Thread.sleep(20);
        triesLeft--;
      }

      if (reader.ready())
        return Integer.parseInt(reader.readLine());
      else
        return 0;
    }
    catch (InterruptedException e)
    {
      throw new RuntimeException(e);
    }
  }

  public void addTestStatusListener(TestStatusListener listener)
  {
    eventSupport.addTestStatusListener(listener);
  }

  public void runTest(String testClass)
  {
    try
    {
      List<TestEvent> result = null;
      do
      {
        result = sendTestToProcess(testClass);
        if (ranOutOfMemory(result))
        {
          killTest();
          result = null;
          maxMemoryInMegs *= 2;
        }
      } while (!killTestProcess && result == null && maxMemoryInMegs <= MAX_MEMORY_ALLOWED);
      if (result == null)
        fireOutOfMemoryEvent(testClass);
      else
        processEvents((List<TestEvent>) result);
    }
    catch (UnknownHostException e)
    {
      throw new RuntimeException(e);
    }
    catch (SocketException e)
    {
      if (killTestProcess)
      {
        System.out.println("Socket connection to runner process forcibly closed.");
      }
      else
        throw new RuntimeException(e);
    }
    catch (IOException e)
    {
      throw new RuntimeException(e);
    }
    catch (ClassNotFoundException e)
    {
      throw new RuntimeException(e);
    }
    catch (OutOfMemoryError e)
    {
      fireOutOfMemoryEvent(testClass, (Throwable) e);
    }
    killTestProcess = false;
  }

  private boolean ranOutOfMemory(List<TestEvent> result)
  {
    for (TestEvent event : result)
    {
      if (event.getError() instanceof OutOfMemoryError)
        return true;
    }
    return false;
  }

  private void fireOutOfMemoryEvent(String testClass, Throwable error)
  {
    fireEvent(createErrorEvent(this, testClass, error));
  }

  private void fireOutOfMemoryEvent(String testClass)
  {
    String message = "Test could not be run in a " + MAX_MEMORY_ALLOWED + " megabyte heap space";
    TestEvent e = new TestEvent(this, TestEvent.EventType.TEST_ERROR, message, testClass, "", new OutOfMemoryError(message));
    fireEvent(e);
  }

  private static TestEvent createErrorEvent(Object source, String testClass, Throwable error)
  {
    return new TestEvent(source, TestEvent.EventType.TEST_ERROR, error.getMessage(), testClass, "", error);
  }

  private void processEvents(List<TestEvent> eventList)
  {
    if (eventList.isEmpty())
      throw new IllegalArgumentException("eventList cannot be empty");
    for (TestEvent event : eventList)
    {
      fireEvent(event);
    }
  }

  private List<TestEvent> sendTestToProcess(String testClass) throws UnknownHostException, IOException, ClassNotFoundException
  {
    Socket socket = new Socket("127.0.0.1", currentPort);
    try
    {
      ObjectOutputStream ooStream = new ObjectOutputStream(socket.getOutputStream());
      ObjectInputStream oiStream = new ObjectInputStream(socket.getInputStream());
      ooStream.writeObject(testClass);
      List<TestEvent> result = (List<TestEvent>) oiStream.readObject();
      ooStream.close();
      return result;
    }
    finally
    {
      socket.close();
    }
  }

  public void runTests(Set<String> testClasses)
  {
    for (String testName : testClasses)
    {
      runTest(testName);
    }
  }

  public synchronized void killTest()
  {
    try
    {
      killTestProcess = true;
      process.destroy();
      process.waitFor();
      startProcess();
      killTestProcess = false;
    }
    catch (InterruptedException e)
    {
      throw new RuntimeException(e);
    }
    catch (IOException e)
    {
      throw new RuntimeException(e);
    }
  }

  private void fireEvent(TestEvent event)
  {
    eventSupport.fireTestEvent(event);
  }

  private static class TestRunnerProcess implements TestStatusListener
  {
    private ArrayList<TestEvent> eventList;
    private TestRunner runner;

    public TestRunnerProcess(String runnerClass)
    {
      runner = createRunner(runnerClass);
    }

    private TestRunner createRunner(String string)
    {
      TestRunner runner = null;
      try
      {
        Class runnerClass = Class.forName(string);
        runner = (TestRunner) runnerClass.newInstance();
      }
      catch (ClassNotFoundException e)
      {
        throw new RuntimeException(e);
      }
      catch (InstantiationException e)
      {
        throw new RuntimeException(e);
      }
      catch (IllegalAccessException e)
      {
        throw new RuntimeException(e);
      }
      runner.addTestStatusListener(this);
      return runner;
    }

    public void testCaseSucceeded(TestEvent event)
    {
      processTestEvent(event);
    }

    public void testError(TestEvent event)
    {
      processTestEvent(event);
    }

    public void testFailed(TestEvent event)
    {
      processTestEvent(event);
    }

    public void testMethodSucceeded(TestEvent event)
    {
      processTestEvent(event);
    }

    private void processTestEvent(TestEvent event)
    {
      eventList.add(event);
    }

    private List<TestEvent> runTest(String testName)
    {
      eventList = new ArrayList<TestEvent>();
      runner.runTest(testName);
      return eventList;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException
    {
      try
      {
        TestRunnerProcess process = new TestRunnerProcess(args[0]);
        int portNum = 0;
        if (args.length == 2)
          portNum = Integer.parseInt(args[1]);
        ServerSocket socket = new ServerSocket(portNum);
        System.out.println(socket.getLocalPort());
        while (true)
        {
          Socket clientSocket = socket.accept();
          ObjectInputStream oiStream = new ObjectInputStream(clientSocket.getInputStream());
          ObjectOutputStream ooStream = new ObjectOutputStream(clientSocket.getOutputStream());
          String testName = (String) oiStream.readObject();
          List<TestEvent> results = new ArrayList<TestEvent>();
          try
          {
            results = process.runTest(testName);
            ooStream.writeObject(results);
          }
          catch (Throwable e)
          {
            results.add(createErrorEvent(process, testName, e));
          }
          finally
          {
            ooStream.writeObject(results);
            clientSocket.close();
          }
        }
      }
      catch (Throwable e)
      {
        JOptionPane.showMessageDialog(null, "Fatal error in test process: " + e.getMessage());
      }
    }
  }

  public int getCurrentPort()
  {
    return currentPort;
  }
}
